ποΈGitΠ―ΡΠ°ποΈ
Commit 0632993386561bb99f7d675becfa8f2bf56465b7
Parents : b4ed771
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-07-15T17:25:30-05:00
Committer : GitHub <noreply@github.com>
Date : 2026-07-15T22:25:30Z
revert(map): back out per-waypoint DM/channel recipient picker (#6218) (#6284)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Changes
16 files changed, 25 insertions(+), 693 deletions(-)
Diff
diff --git a/.skills/compose-ui/strings-index.txt b/.skills/compose-ui/strings-index.txt
index bd456f9ea0..1285a90ffb 100644
--- a/.skills/compose-ui/strings-index.txt
+++ b/.skills/compose-ui/strings-index.txt
@@ -1720,14 +1720,10 @@ voltage
wait_for_bluetooth_duration_seconds
wake_on_tap_or_motion
warning
-### WAYPOINT ###
waypoint_delete
waypoint_edit
waypoint_new
waypoint_received
-waypoint_recipient_broadcast
-waypoint_recipient_channel
-waypoint_send_to
weight
### WIFI ###
wifi_config
diff --git a/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt
index 1f39b552c6..155f645727 100644
--- a/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt
+++ b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt
@@ -263,9 +263,6 @@ fun MapView(
// tile-download rectangle machinery) that becomes the draft's bounding box on confirm. Kept distinct from the
// tile-download box (downloadRegionBoundingBox) so the two rectangle modes never collide.
var geofenceBoxDraft by remember { mutableStateOf<Waypoint?>(null) }
- // The recipient selected in EditWaypointDialog before "Set area" tore it down β carried across the round trip
- // so re-opening the dialog with the drawn box doesn't silently reset the destination back to Broadcast.
- var geofenceBoxDraftContactKey by remember { mutableStateOf<String?>(null) }
var geofenceBoxBoundingBox: BoundingBox? by remember { mutableStateOf(null) }
var showDownloadButton: Boolean by remember { mutableStateOf(false) }
@@ -483,10 +480,8 @@ fun MapView(
waypoint.toGeofence() != null && !mapViewModel.isMyWaypoint(id) -> showGeofenceInfoDialog = waypoint
// edit only when unlocked or lockedTo myNodeNum
- waypoint.locked_to in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected -> {
- geofenceBoxDraftContactKey = null
+ waypoint.locked_to in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected ->
showEditWaypointDialog = waypoint
- }
else -> showDeleteWaypointDialog = waypoint
}
@@ -588,7 +583,6 @@ fun MapView(
val enabled = isConnected && downloadRegionBoundingBox == null && geofenceBoxDraft == null
if (enabled) {
- geofenceBoxDraftContactKey = null
showEditWaypointDialog =
Waypoint(latitude_i = (p.latitude * 1e7).toInt(), longitude_i = (p.longitude * 1e7).toInt())
}
@@ -900,16 +894,7 @@ fun MapView(
EditWaypointDialog(
waypoint = showEditWaypointDialog ?: return, // Safe call
displayUnits = displayUnits,
- nodes = nodes,
- ourNode = ourNodeInfo,
- channelSet = channelSet,
- initialContactKey =
- geofenceBoxDraftContactKey
- ?: showEditWaypointDialog
- ?.let { wp -> waypoints[wp.id] }
- ?.let { mapViewModel.waypointContactKey(it) }
- ?: "0${NodeAddress.ID_BROADCAST}",
- onSend = { waypoint, contactKey ->
+ onSend = { waypoint ->
Logger.d { "User clicked send waypoint ${waypoint.id}" }
showEditWaypointDialog = null
@@ -927,7 +912,6 @@ fun MapView(
locked_to = newLockedTo,
icon = newIcon,
),
- contactKey,
)
},
onDelete = { waypoint ->
@@ -939,11 +923,10 @@ fun MapView(
Logger.d { "User clicked cancel marker edit dialog" }
showEditWaypointDialog = null
},
- onBeginBoxAuthoring = { draft, contactKey ->
+ onBeginBoxAuthoring = { draft ->
Logger.d { "User began geofence box authoring for waypoint ${draft.id}" }
showEditWaypointDialog = null
geofenceBoxDraft = draft
- geofenceBoxDraftContactKey = contactKey
map.generateGeofenceBoxOverlay()
},
)
@@ -963,7 +946,6 @@ fun MapView(
if (waypoint.locked_to == 0 && isConnected) {
{
showGeofenceInfoDialog = null
- geofenceBoxDraftContactKey = null
showEditWaypointDialog = waypoint
}
} else {
@@ -997,13 +979,7 @@ fun MapView(
TextButton(
onClick = {
Logger.d { "User deleted waypoint ${waypoint.id} for everyone" }
- // Route the expiry to wherever the waypoint was originally sent (a DM or a secondary
- // channel), not the default broadcast β otherwise a DM'd waypoint's "delete for
- // everyone" never reaches its actual recipient.
- val originalContactKey =
- waypoints[waypoint.id]?.let { mapViewModel.waypointContactKey(it) }
- ?: "0${NodeAddress.ID_BROADCAST}"
- mapViewModel.sendWaypoint(waypoint.copy(expire = 1), originalContactKey)
+ mapViewModel.sendWaypoint(waypoint.copy(expire = 1))
mapViewModel.deleteWaypoint(waypoint.id)
showDeleteWaypointDialog = null
},
diff --git a/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt
index bbfcb37c47..56d247d828 100644
--- a/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt
+++ b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt
@@ -31,7 +31,6 @@ import org.meshtastic.core.repository.NotificationPrefs
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.RadioController
-import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.feature.map.BaseMapViewModel
import java.io.InputStream
@@ -45,7 +44,6 @@ class MapViewModel(
radioController: RadioController,
radioConfigRepository: RadioConfigRepository,
notificationPrefs: NotificationPrefs,
- uiPrefs: UiPrefs,
buildConfigProvider: BuildConfigProvider,
private val mapLayersManager: MapLayersManager,
savedStateHandle: SavedStateHandle,
@@ -56,7 +54,6 @@ class MapViewModel(
radioController,
radioConfigRepository,
notificationPrefs,
- uiPrefs,
) {
private val _selectedWaypointId = MutableStateFlow(savedStateHandle.get<Int>("waypointId"))
diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
index 4108deeeeb..b1ae7a07a1 100644
--- a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
+++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
@@ -114,7 +114,6 @@ import org.meshtastic.app.map.component.WaypointMarkers
import org.meshtastic.app.map.model.NodeClusterItem
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.model.Node
-import org.meshtastic.core.model.NodeAddress
import org.meshtastic.core.model.TracerouteOverlay
import org.meshtastic.core.model.geofence.toGeofence
import org.meshtastic.core.model.util.GeoConstants.DEG_D
@@ -263,9 +262,6 @@ fun MapView(
// --- Geofence box authoring (Main mode) ---
// When non-null, the user is defining a bounding box for [boxAuthoringDraft] by tapping two corners.
var boxAuthoringDraft by remember { mutableStateOf<Waypoint?>(null) }
- // The recipient selected in EditWaypointDialog before box authoring tore it down β carried across the round trip
- // so re-opening the dialog with the drawn box doesn't silently reset the destination back to Broadcast.
- var boxAuthoringDraftContactKey by remember { mutableStateOf<String?>(null) }
var boxAuthoringFirstCorner by remember { mutableStateOf<LatLng?>(null) }
var boxAuthoringSecondCorner by remember { mutableStateOf<LatLng?>(null) }
@@ -350,7 +346,6 @@ fun MapView(
DisposableEffect(Unit) { onDispose { fusedLocationClient.removeLocationUpdates(locationCallback) } }
// --- Node & waypoint data ---
- val nodes by mapViewModel.nodes.collectAsStateWithLifecycle()
val allNodes by mapViewModel.nodesWithPosition.collectAsStateWithLifecycle(listOf())
val waypoints by mapViewModel.waypoints.collectAsStateWithLifecycle(emptyMap())
val displayableWaypoints = waypoints.values.mapNotNull { it.waypoint }
@@ -603,7 +598,6 @@ fun MapView(
},
onMapLongClick = { latLng ->
if (isMainMode && isConnected && boxAuthoringDraft == null) {
- boxAuthoringDraftContactKey = null
editingWaypoint =
Waypoint(
latitude_i = (latLng.latitude / DEG_D).toInt(),
@@ -662,10 +656,7 @@ fun MapView(
displayableWaypoints = displayableWaypoints,
myNodeNum = myNodeNum,
isConnected = isConnected,
- onEditWaypointRequest = {
- boxAuthoringDraftContactKey = null
- editingWaypoint = it
- },
+ onEditWaypointRequest = { editingWaypoint = it },
onShowGeofenceInfo = { geofenceInfoWaypoint = it },
selectedWaypointId = selectedWaypointId,
mapLayers = mapLayers,
@@ -711,14 +702,7 @@ fun MapView(
EditWaypointDialog(
waypoint = waypointToEdit,
displayUnits = displayUnits,
- nodes = nodes,
- ourNode = ourNodeInfo,
- channelSet = channelSet,
- initialContactKey =
- boxAuthoringDraftContactKey
- ?: waypoints[waypointToEdit.id]?.let { mapViewModel.waypointContactKey(it) }
- ?: "0${NodeAddress.ID_BROADCAST}",
- onSend = { updatedWp, contactKey ->
+ onSend = { updatedWp ->
var finalWp = updatedWp
if (updatedWp.id == 0) {
finalWp = finalWp.copy(id = mapViewModel.generatePacketId())
@@ -726,26 +710,19 @@ fun MapView(
if (updatedWp.icon == 0) {
finalWp = finalWp.copy(icon = 0x1F4CD)
}
- mapViewModel.sendWaypoint(finalWp, contactKey)
+ mapViewModel.sendWaypoint(finalWp)
editingWaypoint = null
},
onDelete = { wpToDelete ->
if (wpToDelete.locked_to == 0 && isConnected && wpToDelete.id != 0) {
- // Route the expiry to wherever the waypoint was originally sent (a DM or a secondary
- // channel), not the default broadcast β otherwise a DM'd waypoint's deletion never
- // reaches its actual recipient.
- val originalContactKey =
- waypoints[wpToDelete.id]?.let { mapViewModel.waypointContactKey(it) }
- ?: "0${NodeAddress.ID_BROADCAST}"
- mapViewModel.sendWaypoint(wpToDelete.copy(expire = 1), originalContactKey)
+ mapViewModel.sendWaypoint(wpToDelete.copy(expire = 1))
}
mapViewModel.deleteWaypoint(wpToDelete.id)
editingWaypoint = null
},
onDismissRequest = { editingWaypoint = null },
- onBeginBoxAuthoring = { draft, contactKey ->
+ onBeginBoxAuthoring = { draft ->
boxAuthoringDraft = draft
- boxAuthoringDraftContactKey = contactKey
boxAuthoringFirstCorner = null
boxAuthoringSecondCorner = null
editingWaypoint = null
@@ -767,7 +744,6 @@ fun MapView(
if (waypoint.locked_to == 0 && isConnected) {
{
geofenceInfoWaypoint = null
- boxAuthoringDraftContactKey = null
editingWaypoint = waypoint
}
} else {
diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt
index c8b8b090f1..5015ef00c0 100644
--- a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt
+++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt
@@ -97,7 +97,6 @@ class MapViewModel(
radioController,
radioConfigRepository,
notificationPrefs,
- uiPrefs,
) {
private val _selectedWaypointId = MutableStateFlow(savedStateHandle.get<Int>("waypointId"))
diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeSortOption.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeSortOption.kt
index 3e2b654211..656da0cc33 100644
--- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeSortOption.kt
+++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NodeSortOption.kt
@@ -34,10 +34,4 @@ enum class NodeSortOption(val sqlValue: String, val stringRes: StringResource) {
CHANNEL("channel", Res.string.node_sort_channel),
VIA_MQTT("via_mqtt", Res.string.node_sort_via_mqtt),
VIA_FAVORITE("via_favorite", Res.string.node_sort_via_favorite),
- ;
-
- companion object {
- /** Maps a persisted preference ordinal (e.g. `UiPrefs.nodeSort`) back to its option. */
- fun fromOrdinal(ordinal: Int): NodeSortOption = entries.getOrElse(ordinal) { VIA_FAVORITE }
- }
}
diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt
index 09201a6676..a84cac27d3 100644
--- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt
+++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt
@@ -16,11 +16,8 @@
*/
package org.meshtastic.core.repository
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
import org.meshtastic.core.model.DeviceType
-import org.meshtastic.core.model.NodeSortOption
/** Reactive interface for analytics-related preferences. */
interface AnalyticsPrefs {
@@ -197,10 +194,6 @@ interface UiPrefs {
fun setShouldShowTelemetry(value: Boolean)
}
-/** Maps the persisted [UiPrefs.nodeSort] ordinal to its [NodeSortOption], the single source every consumer shares. */
-val UiPrefs.nodeSortOption: Flow<NodeSortOption>
- get() = nodeSort.map { NodeSortOption.fromOrdinal(it) }
-
/** Reactive interface for notification preferences. */
interface NotificationPrefs {
val messagesEnabled: StateFlow<Boolean>
diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml
index d9f4fc9e08..ae7676d4a6 100644
--- a/core/resources/src/commonMain/composeResources/values/strings.xml
+++ b/core/resources/src/commonMain/composeResources/values/strings.xml
@@ -1765,14 +1765,10 @@
<string name="wait_for_bluetooth_duration_seconds">Wait for Bluetooth duration</string>
<string name="wake_on_tap_or_motion">Wake on tap or motion</string>
<string name="warning">Warning</string>
- <!-- WAYPOINT -->
<string name="waypoint_delete">Delete waypoint?</string>
<string name="waypoint_edit">Edit waypoint</string>
<string name="waypoint_new">New waypoint</string>
<string name="waypoint_received">Received waypoint: %1$s</string>
- <string name="waypoint_recipient_broadcast">Broadcast (all nodes)</string>
- <string name="waypoint_recipient_channel">Channel %1$d</string>
- <string name="waypoint_send_to">Send to</string>
<string name="weight">Weight</string>
<!-- WIFI -->
<string name="wifi_config">WiFi Options</string>
diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts
index d6dfea310d..63ce33a721 100644
--- a/feature/map/build.gradle.kts
+++ b/feature/map/build.gradle.kts
@@ -37,16 +37,5 @@ kotlin {
implementation(projects.core.ui)
implementation(projects.core.di)
}
-
- getByName("androidHostTest") {
- dependencies {
- implementation(projects.core.testing)
- implementation(libs.kotlinx.coroutines.test)
- implementation(libs.compose.multiplatform.ui.test)
- // Registers a ComponentActivity in the test manifest so Robolectric's runComposeUiTest has a host
- // activity to attach to (feature/map is a library module with no launcher activity of its own).
- implementation(libs.androidx.compose.ui.test.manifest)
- }
- }
}
}
diff --git a/feature/map/src/androidHostTest/kotlin/org/meshtastic/feature/map/component/EditWaypointDialogTest.kt b/feature/map/src/androidHostTest/kotlin/org/meshtastic/feature/map/component/EditWaypointDialogTest.kt
deleted file mode 100644
index 2ded8f0371..0000000000
--- a/feature/map/src/androidHostTest/kotlin/org/meshtastic/feature/map/component/EditWaypointDialogTest.kt
+++ /dev/null
@@ -1,252 +0,0 @@
-/*
- * Copyright (c) 2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-package org.meshtastic.feature.map.component
-
-import androidx.compose.ui.test.ExperimentalTestApi
-import androidx.compose.ui.test.assertCountEquals
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.onAllNodesWithText
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.performTextInput
-import androidx.compose.ui.test.v2.runComposeUiTest
-import okio.ByteString.Companion.encodeUtf8
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.meshtastic.core.model.NodeAddress
-import org.meshtastic.core.resources.Res
-import org.meshtastic.core.resources.getString
-import org.meshtastic.core.resources.node_filter_placeholder
-import org.meshtastic.core.resources.send
-import org.meshtastic.core.resources.waypoint_recipient_broadcast
-import org.meshtastic.core.testing.TestDataFactory
-import org.meshtastic.proto.ChannelSet
-import org.meshtastic.proto.ChannelSettings
-import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits
-import org.meshtastic.proto.Waypoint
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.annotation.Config
-import kotlin.test.assertEquals
-
-@OptIn(ExperimentalTestApi::class)
-@RunWith(RobolectricTestRunner::class)
-@Config(sdk = [34])
-class EditWaypointDialogTest {
-
- private val waypoint = Waypoint(id = 1, name = "Test Waypoint")
- private val node = TestDataFactory.createTestNode(num = 42, userId = "!a1b2c3d4", longName = "Alice")
- private val channelSet =
- ChannelSet(settings = listOf(ChannelSettings(name = "Primary"), ChannelSettings(name = "Secondary")))
-
- @Test
- fun sendDefaultsToPrimaryChannel() = runComposeUiTest {
- var sentContactKey: String? = null
- setContent {
- EditWaypointDialog(
- waypoint = waypoint,
- displayUnits = DisplayUnits.METRIC,
- nodes = listOf(node),
- ourNode = null,
- channelSet = channelSet,
- onSend = { _, contactKey -> sentContactKey = contactKey },
- onDelete = {},
- onDismissRequest = {},
- )
- }
-
- // The primary channel (index 0) is labelled with its configured name, not a generic "Broadcast" string.
- onNodeWithText("Primary").assertIsDisplayed()
- onNodeWithText(getString(Res.string.send)).performClick()
-
- assertEquals("0${NodeAddress.ID_BROADCAST}", sentContactKey)
- }
-
- @Test
- fun sendDefaultsToGenericBroadcastLabelWhenChannelSetUnavailable() = runComposeUiTest {
- setContent {
- EditWaypointDialog(
- waypoint = waypoint,
- displayUnits = DisplayUnits.METRIC,
- nodes = listOf(node),
- ourNode = null,
- channelSet = null,
- onSend = { _, _ -> },
- onDelete = {},
- onDismissRequest = {},
- )
- }
-
- // No channel info (e.g. disconnected) falls back to the generic "Broadcast" label.
- onNodeWithText(getString(Res.string.waypoint_recipient_broadcast)).assertIsDisplayed()
- }
-
- @Test
- fun sendOffersOnlyBroadcastWhenChannelSetSettingsIsEmpty() = runComposeUiTest {
- setContent {
- EditWaypointDialog(
- waypoint = waypoint,
- displayUnits = DisplayUnits.METRIC,
- nodes = listOf(node),
- ourNode = null,
- // Non-null but empty settings (e.g. right after a fresh install, before channel sync completes) must
- // still offer a Broadcast option rather than rendering zero channel rows.
- channelSet = ChannelSet(settings = emptyList()),
- onSend = { _, _ -> },
- onDelete = {},
- onDismissRequest = {},
- )
- }
-
- onNodeWithText(getString(Res.string.waypoint_recipient_broadcast)).assertIsDisplayed()
- }
-
- @Test
- fun pickingANonPkcNodeRoutesViaItsOwnChannel() = runComposeUiTest {
- var sentContactKey: String? = null
- setContent {
- EditWaypointDialog(
- waypoint = waypoint,
- displayUnits = DisplayUnits.METRIC,
- nodes = listOf(node),
- ourNode = null,
- channelSet = channelSet,
- onSend = { _, contactKey -> sentContactKey = contactKey },
- onDelete = {},
- onDismissRequest = {},
- )
- }
-
- // The clickable element is the TextButton showing the *current* recipient value ("Primary" before any
- // selection), not the static "Send to" label beside it.
- onNodeWithText("Primary").performClick()
- onNodeWithText("Alice").assertIsDisplayed()
- onNodeWithText("Alice").performClick()
- onNodeWithText(getString(Res.string.send)).performClick()
-
- // Neither this node nor our local node advertises PKC, so the DM routes on the node's own channel (0),
- // not the PKC channel index β matching NodeListViewModel.getDirectMessageRoute's convention.
- assertEquals("0!a1b2c3d4", sentContactKey)
- }
-
- @Test
- fun pickingAPkcCapableNodeRoutesViaPkcChannel() = runComposeUiTest {
- var sentContactKey: String? = null
- val pkcNode = node.copy(publicKey = "node-key".encodeUtf8())
- val ourPkcNode =
- TestDataFactory.createTestNode(num = 1, userId = "!local0001").copy(publicKey = "our-key".encodeUtf8())
- setContent {
- EditWaypointDialog(
- waypoint = waypoint,
- displayUnits = DisplayUnits.METRIC,
- nodes = listOf(pkcNode),
- ourNode = ourPkcNode,
- channelSet = channelSet,
- onSend = { _, contactKey -> sentContactKey = contactKey },
- onDelete = {},
- onDismissRequest = {},
- )
- }
-
- onNodeWithText("Primary").performClick()
- onNodeWithText("Alice").performClick()
- onNodeWithText(getString(Res.string.send)).performClick()
-
- // Both ends support PKC, so the DM routes on the dedicated PKC channel index.
- assertEquals("${NodeAddress.PKC_CHANNEL_INDEX}!a1b2c3d4", sentContactKey)
- }
-
- @Test
- fun pickingASecondaryChannelRoutesSendToThatChannel() = runComposeUiTest {
- var sentContactKey: String? = null
- setContent {
- EditWaypointDialog(
- waypoint = waypoint,
- displayUnits = DisplayUnits.METRIC,
- nodes = listOf(node),
- ourNode = null,
- channelSet = channelSet,
- onSend = { _, contactKey -> sentContactKey = contactKey },
- onDelete = {},
- onDismissRequest = {},
- )
- }
-
- onNodeWithText("Primary").performClick()
- onNodeWithText("Secondary").assertIsDisplayed()
- onNodeWithText("Secondary").performClick()
- onNodeWithText(getString(Res.string.send)).performClick()
-
- assertEquals("1${NodeAddress.ID_BROADCAST}", sentContactKey)
- }
-
- @Test
- fun initialContactKeySeedsTheSelectedRecipient() = runComposeUiTest {
- var sentContactKey: String? = null
- setContent {
- EditWaypointDialog(
- waypoint = waypoint,
- displayUnits = DisplayUnits.METRIC,
- nodes = listOf(node),
- ourNode = null,
- channelSet = channelSet,
- // Simulates re-opening the dialog after geofence box authoring with a previously-picked recipient.
- initialContactKey = "1${NodeAddress.ID_BROADCAST}",
- onSend = { _, contactKey -> sentContactKey = contactKey },
- onDelete = {},
- onDismissRequest = {},
- )
- }
-
- onNodeWithText("Secondary").assertIsDisplayed()
- onNodeWithText(getString(Res.string.send)).performClick()
-
- assertEquals("1${NodeAddress.ID_BROADCAST}", sentContactKey)
- }
-
- @Test
- fun filteringRecipientPickerNarrowsNodesButKeepsChannelsVisible() = runComposeUiTest {
- val bob = TestDataFactory.createTestNode(num = 43, userId = "!b1c2d3e4", longName = "Bob", shortName = "B")
- setContent {
- EditWaypointDialog(
- waypoint = waypoint,
- displayUnits = DisplayUnits.METRIC,
- nodes = listOf(node, bob),
- ourNode = null,
- channelSet = channelSet,
- onSend = { _, _ -> },
- onDelete = {},
- onDismissRequest = {},
- )
- }
-
- onNodeWithText("Primary").performClick()
- onNodeWithText("Alice").assertIsDisplayed()
- onNodeWithText("Bob").assertIsDisplayed()
-
- onNodeWithText(getString(Res.string.node_filter_placeholder)).performTextInput("Bob")
-
- // Filtering by node name narrows the node list. "Bob" now matches two elements β the filter field's own
- // typed-in value, and the surviving node row β so assert the count directly rather than picking an
- // arbitrary match by index, which would risk silently checking the wrong element.
- onAllNodesWithText("Bob").assertCountEquals(2)
- onAllNodesWithText("Alice").assertCountEquals(0)
- // ...but channel rows are never filtered out. "Primary" also matches the (now-covered) "Send to" button
- // behind the dialog, so likewise assert the count (button + picker row) instead of an arbitrary index.
- onAllNodesWithText("Primary").assertCountEquals(2)
- onNodeWithText("Secondary").assertIsDisplayed()
- }
-}
diff --git a/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt b/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
index deb3895c7d..49dfeaffc8 100644
--- a/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
+++ b/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
@@ -55,7 +55,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -75,11 +74,7 @@ import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.systemTimeZone
-import org.meshtastic.core.model.ContactKey
-import org.meshtastic.core.model.Node
-import org.meshtastic.core.model.NodeAddress
import org.meshtastic.core.model.geofence.GeofenceRadiusPresets
-import org.meshtastic.core.model.util.getChannel
import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.cancel
@@ -102,69 +97,19 @@ import org.meshtastic.core.resources.send
import org.meshtastic.core.resources.time
import org.meshtastic.core.resources.waypoint_edit
import org.meshtastic.core.resources.waypoint_new
-import org.meshtastic.core.resources.waypoint_recipient_broadcast
-import org.meshtastic.core.resources.waypoint_recipient_channel
-import org.meshtastic.core.resources.waypoint_send_to
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
import org.meshtastic.core.ui.icon.CalendarMonth
import org.meshtastic.core.ui.icon.Lock
import org.meshtastic.core.ui.icon.MeshtasticIcons
-import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits
import org.meshtastic.proto.Waypoint
import kotlin.time.Duration.Companion.hours
-/** Contact key for the broadcast destination on the primary channel β the historical default for waypoints. */
-private val BROADCAST_CONTACT_KEY = ContactKey.broadcast(0).value
-
-/**
- * Number of selectable channel slots in [channelSet]: at least 1, so a channel-less/disconnected state still offers
- * Broadcast.
- */
-internal fun channelCount(channelSet: ChannelSet?): Int = channelSet?.settings?.size?.takeIf { it > 0 } ?: 1
-
-/**
- * Display name for channel [index] in [channelSet]: its configured name, or "Broadcast" (index 0) / "Channel N" as a
- * fallback.
- */
-@Composable
-internal fun channelLabel(channelSet: ChannelSet?, index: Int): String {
- val configuredName = channelSet?.getChannel(index)?.name
- return configuredName
- ?: if (index == 0) {
- stringResource(Res.string.waypoint_recipient_broadcast)
- } else {
- stringResource(Res.string.waypoint_recipient_channel, index)
- }
-}
-
-/**
- * Display name for the recipient identified by [contactKey]: the destination channel's configured name (including the
- * primary channel, e.g. "LongFast" β falling back to "Broadcast" only when no channel info is available at all, such as
- * while disconnected), a DM node's long/short name, or β if that node is no longer in [nodes] β its raw id, so a stale
- * selection never misreports itself as "Broadcast".
- */
-@Composable
-private fun recipientLabel(contactKey: String, nodes: List<Node>, ourNode: Node?, channelSet: ChannelSet?): String {
- val parsed = ContactKey(contactKey)
- return if (parsed.address is NodeAddress.Broadcast) {
- channelLabel(channelSet, parsed.channel)
- } else {
- val node = nodes.find { dmContactKey(it, ourNode) == contactKey }
- node?.user?.long_name?.takeIf { it.isNotBlank() }
- ?: node?.user?.short_name?.takeIf { it.isNotBlank() }
- ?: (parsed.address as? NodeAddress.ById)?.id
- ?: stringResource(Res.string.waypoint_recipient_broadcast)
- }
-}
-
/**
* Shared waypoint editor used by both the google and fdroid map flavors (DRY β replaces the two drifted per-flavor
* copies). Map-engine-specific concerns stay outside: drawing the box overlay and the drag-to-define gesture are
- * triggered via [onBeginBoxAuthoring], which hands the current draft *and the selected recipient* back to the flavor's
- * map so the user can define the bounding box there; the flavor re-opens this dialog (passing the same recipient back
- * as [initialContactKey]) with the drawn box applied β otherwise the recipient choice would silently reset to Broadcast
- * every time the user draws a geofence area.
+ * triggered via [onBeginBoxAuthoring], which hands the current draft back to the flavor's map so the user can define
+ * the bounding box there; the flavor re-opens this dialog with the drawn box applied.
*/
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod", "MagicNumber")
@@ -172,23 +117,17 @@ private fun recipientLabel(contactKey: String, nodes: List<Node>, ourNode: Node?
fun EditWaypointDialog(
waypoint: Waypoint,
displayUnits: DisplayUnits,
- nodes: List<Node>,
- ourNode: Node?,
- channelSet: ChannelSet?,
- onSend: (Waypoint, contactKey: String) -> Unit,
+ onSend: (Waypoint) -> Unit,
onDelete: (Waypoint) -> Unit,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
- initialContactKey: String = BROADCAST_CONTACT_KEY,
- onBeginBoxAuthoring: (Waypoint, contactKey: String) -> Unit = { _, _ -> },
+ onBeginBoxAuthoring: (Waypoint) -> Unit = {},
) {
- var waypointInput by remember(waypoint.id) { mutableStateOf(waypoint) }
+ var waypointInput by remember { mutableStateOf(waypoint) }
val title = if (waypoint.id == 0) Res.string.waypoint_new else Res.string.waypoint_edit
val defaultEmoji = 0x1F4CD // π Round Pushpin
val currentEmojiCodepoint = if (waypointInput.icon == 0) defaultEmoji else waypointInput.icon
var showEmojiPickerView by remember { mutableStateOf(false) }
- var showRecipientPicker by rememberSaveable { mutableStateOf(false) }
- var selectedContactKey by rememberSaveable(waypoint.id) { mutableStateOf(initialContactKey) }
val context = LocalContext.current
val tz = systemTimeZone
@@ -271,17 +210,6 @@ fun EditWaypointDialog(
maxLines = 3,
)
Spacer(modifier = Modifier.size(8.dp))
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
- ) {
- Text(stringResource(Res.string.waypoint_send_to))
- TextButton(onClick = { showRecipientPicker = true }) {
- Text(recipientLabel(selectedContactKey, nodes, ourNode, channelSet))
- }
- }
- Spacer(modifier = Modifier.size(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
@@ -443,7 +371,7 @@ fun EditWaypointDialog(
waypoint = waypointInput,
displayUnits = displayUnits,
onWaypointChange = { waypointInput = it },
- onBeginBoxAuthoring = { onBeginBoxAuthoring(waypointInput, selectedContactKey) },
+ onBeginBoxAuthoring = { onBeginBoxAuthoring(waypointInput) },
)
}
},
@@ -461,10 +389,7 @@ fun EditWaypointDialog(
TextButton(onClick = onDismissRequest, modifier = Modifier.padding(end = 8.dp)) {
Text(stringResource(Res.string.cancel))
}
- Button(
- onClick = { onSend(waypointInput, selectedContactKey) },
- enabled = (waypointInput.name).isNotBlank(),
- ) {
+ Button(onClick = { onSend(waypointInput) }, enabled = (waypointInput.name).isNotBlank()) {
Text(stringResource(Res.string.send))
}
}
@@ -478,20 +403,6 @@ fun EditWaypointDialog(
waypointInput = waypointInput.copy(icon = selectedEmoji.codePointAt(0))
}
}
-
- if (showRecipientPicker) {
- WaypointRecipientPickerDialog(
- nodes = nodes,
- ourNode = ourNode,
- channelSet = channelSet,
- selectedContactKey = selectedContactKey,
- onSelect = {
- selectedContactKey = it
- showRecipientPicker = false
- },
- onDismissRequest = { showRecipientPicker = false },
- )
- }
}
/**
diff --git a/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/component/WaypointRecipientPicker.kt b/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/component/WaypointRecipientPicker.kt
deleted file mode 100644
index 73a6572aaa..0000000000
--- a/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/component/WaypointRecipientPicker.kt
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright (c) 2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-package org.meshtastic.feature.map.component
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.selection.toggleable
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.RadioButton
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.unit.dp
-import org.jetbrains.compose.resources.stringResource
-import org.meshtastic.core.model.ContactKey
-import org.meshtastic.core.model.Node
-import org.meshtastic.core.model.NodeAddress
-import org.meshtastic.core.resources.Res
-import org.meshtastic.core.resources.cancel
-import org.meshtastic.core.resources.node_filter_placeholder
-import org.meshtastic.core.resources.waypoint_send_to
-import org.meshtastic.proto.ChannelSet
-
-/**
- * Direct-message contact key for sending to [node]. Uses the PKC channel index only when both ends support it β mirrors
- * [org.meshtastic.feature.node.list.NodeListViewModel.getDirectMessageRoute], the app's other DM-key builder β
- * otherwise falls back to the node's own channel, matching how a DM would actually route on the mesh.
- */
-internal fun dmContactKey(node: Node, ourNode: Node?): String {
- val hasPKC = ourNode?.hasPKC == true && node.hasPKC
- val channel = if (hasPKC) NodeAddress.PKC_CHANNEL_INDEX else node.channel
- return "$channel${node.user.id}"
-}
-
-/**
- * Nodes whose long or short name contains [filterText] (case-insensitive); all of [nodes] when [filterText] is blank.
- */
-private fun filterNodesByName(nodes: List<Node>, filterText: String): List<Node> = if (filterText.isBlank()) {
- nodes
-} else {
- nodes.filter {
- it.user.long_name.contains(filterText, ignoreCase = true) ||
- it.user.short_name.contains(filterText, ignoreCase = true)
- }
-}
-
-/** Recipient-picker row label for [node]: long name, short name, or a derived id β never blank. */
-private fun nodeRowLabel(node: Node): String = node.user.long_name.takeIf { it.isNotBlank() }
- ?: node.user.short_name.takeIf { it.isNotBlank() }
- ?: NodeAddress.numToDefaultId(node.num)
-
-/**
- * Lists the primary channel (labelled with its configured name, e.g. "LongFast") plus any secondary channels, then
- * every known [nodes] entry β letting the user pick a waypoint destination the same way the Python Meshtastic CLI/API
- * can target a channel or a specific node.
- */
-@Composable
-internal fun WaypointRecipientPickerDialog(
- nodes: List<Node>,
- ourNode: Node?,
- channelSet: ChannelSet?,
- selectedContactKey: String,
- onSelect: (String) -> Unit,
- onDismissRequest: () -> Unit,
-) {
- val distinctNodes = nodes.distinctBy { it.num }
- var filterText by rememberSaveable { mutableStateOf("") }
- val filteredNodes = remember(distinctNodes, filterText) { filterNodesByName(distinctNodes, filterText) }
-
- AlertDialog(
- onDismissRequest = onDismissRequest,
- title = { Text(stringResource(Res.string.waypoint_send_to)) },
- text = {
- Column {
- OutlinedTextField(
- value = filterText,
- onValueChange = { filterText = it },
- label = { Text(stringResource(Res.string.node_filter_placeholder)) },
- singleLine = true,
- modifier = Modifier.fillMaxWidth(),
- )
- Spacer(modifier = Modifier.size(8.dp))
- LazyColumn(modifier = Modifier.heightIn(max = 400.dp)) {
- items(count = channelCount(channelSet), key = { "channel_$it" }) { channelIndex ->
- val contactKey = ContactKey.broadcast(channelIndex).value
- RecipientRow(
- label = channelLabel(channelSet, channelIndex),
- selected = selectedContactKey == contactKey,
- onClick = { onSelect(contactKey) },
- )
- }
- if (filteredNodes.isNotEmpty()) {
- item {
- Spacer(modifier = Modifier.size(8.dp))
- HorizontalDivider()
- Spacer(modifier = Modifier.size(8.dp))
- }
- }
- items(filteredNodes, key = { it.num }) { node ->
- val contactKey = dmContactKey(node, ourNode)
- RecipientRow(
- label = nodeRowLabel(node),
- selected = selectedContactKey == contactKey,
- onClick = { onSelect(contactKey) },
- )
- }
- }
- }
- },
- confirmButton = { TextButton(onClick = onDismissRequest) { Text(stringResource(Res.string.cancel)) } },
- dismissButton = null,
- )
-}
-
-@Composable
-private fun RecipientRow(label: String, selected: Boolean, onClick: () -> Unit) {
- Row(
- modifier =
- Modifier.fillMaxWidth()
- .toggleable(value = selected, role = Role.RadioButton, onValueChange = { onClick() }),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- RadioButton(selected = selected, onClick = null)
- Spacer(modifier = Modifier.width(8.dp))
- Text(label)
- }
-}
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt
index 85a5761fef..aabba7f836 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt
@@ -17,12 +17,10 @@
package org.meshtastic.feature.map
import androidx.lifecycle.ViewModel
-import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import org.jetbrains.compose.resources.StringResource
@@ -34,7 +32,6 @@ import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeAddress
import org.meshtastic.core.model.TracerouteOverlay
import org.meshtastic.core.model.geofence.activeWaypointPackets
-import org.meshtastic.core.model.isBroadcast
import org.meshtastic.core.model.isFromLocal
import org.meshtastic.core.model.util.DistanceUnit
import org.meshtastic.core.repository.MapPrefs
@@ -43,8 +40,6 @@ import org.meshtastic.core.repository.NotificationPrefs
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.RadioController
-import org.meshtastic.core.repository.UiPrefs
-import org.meshtastic.core.repository.nodeSortOption
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.any
import org.meshtastic.core.resources.eight_hours
@@ -72,7 +67,6 @@ open class BaseMapViewModel(
private val radioController: RadioController,
private val radioConfigRepository: RadioConfigRepository,
private val notificationPrefs: NotificationPrefs,
- private val uiPrefs: UiPrefs,
) : ViewModel() {
val myNodeInfo = nodeRepository.myNodeInfo
@@ -102,13 +96,9 @@ open class BaseMapViewModel(
.map { it is org.meshtastic.core.model.ConnectionState.Connected }
.stateInWhileSubscribed(initialValue = false)
- /**
- * Nodes sorted per the user's current Nodes-tab sort preference (re-queried live whenever that preference changes,
- * e.g. via the waypoint recipient picker staying in sync with the Nodes tab).
- */
val nodes: StateFlow<List<Node>> =
- uiPrefs.nodeSortOption
- .flatMapLatest { sort -> nodeRepository.getNodes(sort = sort) }
+ nodeRepository
+ .getNodes()
.map { nodes -> nodes.filterNot { node -> node.isIgnored } }
.stateInWhileSubscribed(initialValue = emptyList())
@@ -186,25 +176,16 @@ open class BaseMapViewModel(
fun deleteWaypoint(id: Int) =
safeLaunch(context = ioDispatcher, tag = "deleteWaypoint") { packetRepository.deleteWaypoint(id) }
- /**
- * Contact key a waypoint packet was exchanged on: its destination for packets we sent (or broadcasts), otherwise
- * its sender β the same derivation as MeshDataHandlerImpl.rememberDataPacket, so edits and expiries route back to
- * the conversation the waypoint actually lives in (a raw `to` would self-address a waypoint someone DM'd to us).
- */
- fun waypointContactKey(packet: DataPacket): String {
- val contact = if (packet.isFromLocal(myNodeNum) || packet.isBroadcast) packet.to else packet.from
- return "${packet.channel}$contact"
- }
-
- fun sendWaypoint(wpt: Waypoint, contactKey: String = "0${NodeAddress.ID_BROADCAST}"): Job? {
+ fun sendWaypoint(wpt: Waypoint, contactKey: String = "0${NodeAddress.ID_BROADCAST}") {
// contactKey: unique contact key filter (channel)+(nodeId)
val parsedKey = ContactKey(contactKey)
val p = DataPacket(parsedKey.addressString, parsedKey.channel, wpt)
- return if (wpt.id != 0) sendDataPacket(p) else null
+ if (wpt.id != 0) sendDataPacket(p)
}
- private fun sendDataPacket(p: DataPacket): Job =
+ private fun sendDataPacket(p: DataPacket) {
safeLaunch(context = ioDispatcher, tag = "sendDataPacket") { radioController.sendMessage(p) }
+ }
fun generatePacketId(): Int = radioController.generatePacketId()
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt
index 5e5f533e6d..482e7702f6 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/SharedMapViewModel.kt
@@ -23,7 +23,6 @@ import org.meshtastic.core.repository.NotificationPrefs
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.RadioController
-import org.meshtastic.core.repository.UiPrefs
@KoinViewModel
class SharedMapViewModel(
@@ -33,7 +32,6 @@ class SharedMapViewModel(
radioController: RadioController,
radioConfigRepository: RadioConfigRepository,
notificationPrefs: NotificationPrefs,
- uiPrefs: UiPrefs,
) : BaseMapViewModel(
mapPrefs,
nodeRepository,
@@ -41,5 +39,4 @@ class SharedMapViewModel(
radioController,
radioConfigRepository,
notificationPrefs,
- uiPrefs,
)
diff --git a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/BaseMapViewModelTest.kt b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/BaseMapViewModelTest.kt
index b26092dcd4..909c3bcb28 100644
--- a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/BaseMapViewModelTest.kt
+++ b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/BaseMapViewModelTest.kt
@@ -31,14 +31,12 @@ import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.NodeAddress
-import org.meshtastic.core.model.NodeSortOption
import org.meshtastic.core.repository.MapPrefs
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.testing.FakeNodeRepository
import org.meshtastic.core.testing.FakeNotificationPrefs
import org.meshtastic.core.testing.FakeRadioConfigRepository
import org.meshtastic.core.testing.FakeRadioController
-import org.meshtastic.core.testing.FakeUiPrefs
import org.meshtastic.core.testing.TestDataFactory
import org.meshtastic.proto.Waypoint
import kotlin.test.AfterTest
@@ -46,7 +44,6 @@ import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
-import kotlin.test.assertNull
@OptIn(ExperimentalCoroutinesApi::class)
class BaseMapViewModelTest {
@@ -85,7 +82,6 @@ class BaseMapViewModelTest {
radioController = radioController,
radioConfigRepository = radioConfigRepository,
notificationPrefs = FakeNotificationPrefs(),
- uiPrefs = FakeUiPrefs(),
)
}
@@ -138,66 +134,6 @@ class BaseMapViewModelTest {
assertEquals(3, nodeRepository.nodeDBbyNum.value.size)
}
- @Test
- fun testNodesResortWhenSortPreferenceChanges() = runTest(testDispatcher) {
- val uiPrefs = FakeUiPrefs()
- val vm =
- BaseMapViewModel(
- mapPrefs = mapPrefs,
- nodeRepository = nodeRepository,
- packetRepository = packetRepository,
- radioController = radioController,
- radioConfigRepository = radioConfigRepository,
- notificationPrefs = FakeNotificationPrefs(),
- uiPrefs = uiPrefs,
- )
- // Alice sorts first alphabetically, but Zoe was heard more recently β the two sort modes disagree, so a
- // resort is observable.
- val alice = TestDataFactory.createTestNode(num = 1, userId = "!a", longName = "Alice", lastHeard = 100)
- val zoe = TestDataFactory.createTestNode(num = 2, userId = "!z", longName = "Zoe", lastHeard = 200)
- nodeRepository.setNodes(listOf(alice, zoe))
-
- vm.nodes.test {
- // Default sort (LAST_HEARD): most-recently-heard first.
- assertEquals(listOf("Zoe", "Alice"), awaitItem().map { it.user.long_name })
-
- // Switching the Nodes-tab sort preference re-sorts the map's node list live, without a new
- // subscription.
- uiPrefs.setNodeSort(NodeSortOption.ALPHABETICAL.ordinal)
- assertEquals(listOf("Alice", "Zoe"), awaitItem().map { it.user.long_name })
-
- cancelAndIgnoreRemainingEvents()
- }
- }
-
- @Test
- fun testSendWaypointDefaultsToBroadcast() = runTest(testDispatcher) {
- viewModel.sendWaypoint(Waypoint(id = 1, name = "Broadcast Waypoint"))?.join()
-
- val sent = radioController.sentPackets.single()
- assertEquals(NodeAddress.ID_BROADCAST, sent.to)
- assertEquals(0, sent.channel)
- }
-
- @Test
- fun testSendWaypointWithContactKeyRoutesToNode() = runTest(testDispatcher) {
- val contactKey = "8!a1b2c3d4"
-
- viewModel.sendWaypoint(Waypoint(id = 2, name = "DM Waypoint"), contactKey)?.join()
-
- val sent = radioController.sentPackets.single()
- assertEquals("!a1b2c3d4", sent.to)
- assertEquals(8, sent.channel)
- }
-
- @Test
- fun testSendWaypointWithZeroIdDoesNotSend() = runTest(testDispatcher) {
- val job = viewModel.sendWaypoint(Waypoint(id = 0, name = "Unsaved Draft"))
-
- assertNull(job)
- assertEquals(emptyList(), radioController.sentPackets)
- }
-
@Test
fun testWaypointsIncludeFutureExpirations() = runTest(testDispatcher) {
val now = nowSeconds.toInt()
diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt
index cf4af287cd..b3568e2869 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt
@@ -16,10 +16,10 @@
*/
package org.meshtastic.feature.node.list
+import kotlinx.coroutines.flow.map
import org.koin.core.annotation.Single
import org.meshtastic.core.model.NodeSortOption
import org.meshtastic.core.repository.UiPrefs
-import org.meshtastic.core.repository.nodeSortOption
@Single
@Suppress("TooManyFunctions")
@@ -43,7 +43,8 @@ open class NodeFilterPreferences constructor(private val uiPrefs: UiPrefs) {
open val shouldShowRole = uiPrefs.shouldShowRole
open val shouldShowTelemetry = uiPrefs.shouldShowTelemetry
- open val nodeSortOption = uiPrefs.nodeSortOption
+ open val nodeSortOption =
+ uiPrefs.nodeSort.map { NodeSortOption.entries.getOrElse(it) { NodeSortOption.VIA_FAVORITE } }
open fun setNodeSort(option: NodeSortOption) {
uiPrefs.setNodeSort(option.ordinal)
Served by rngit 1.5.4 - Generated in 0.3s